Creating Structured Forms in Flutter – Detailed Notes
Structured forms are an important part of Flutter application development. They allow developers to collect, organize, validate, and process user information such as names, email addresses, passwords, phone numbers, addresses, selections, and other application data.
Flutter provides the Form, FormField, and TextFormField widgets to create forms where multiple fields can be grouped together and validated or saved as a single unit. A GlobalKey can be used to access the form state and perform operations such as validation, saving, and resetting.
1. What Is a Structured Form?
A structured form is a form whose fields are organized logically into sections according to the type of information being collected.
For example, a registration form can be structured into:
- Personal Information
- Contact Information
- Account Information
- Preferences
- Terms and Conditions
- Submit and Reset Actions
A well-structured form improves readability, usability, validation, and maintainability.
Example Structure
Registration Form
├── Personal Information
│ ├── Full Name
│ └── Date of Birth
├── Contact Information
│ ├── Email
│ └── Phone
├── Account Information
│ ├── Username
│ ├── Password
│ └── Confirm Password
├── Preferences
│ └── Gender
├── Terms
│ └── Accept Terms
└── Actions
├── Submit
└── Reset
2. Why Use Structured Forms?
- Organizes large amounts of user input.
- Makes the UI easier to understand.
- Allows validation of multiple fields.
- Makes form maintenance easier.
- Improves user experience.
- Allows related fields to be grouped together.
- Makes it easier to process submitted data.
- Supports reusable form components.
- Helps developers separate UI sections logically.
3. Important Flutter Form Widgets
| Widget/Class |
Purpose |
Form |
Groups multiple form fields together. |
FormField |
Provides the base functionality for an individual form field. |
TextFormField |
Creates a text input field integrated with a Form. |
DropdownButtonFormField |
Creates a dropdown field that works with forms. |
GlobalKey |
Provides access to the FormState. |
FormState |
Provides methods such as validate(), save(), and reset(). |
TextEditingController |
Controls and retrieves text entered into text fields. |
Flutter's official documentation describes Form as a container for grouping multiple form fields and provides methods through FormState to save, reset, and validate those fields.
4. Basic Structured Form
The following example creates a simple form containing name, email, and password fields.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const RegistrationPage(),
);
}
}
class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key});
@override
State createState() => _RegistrationPageState();
}
class _RegistrationPageState extends State {
final _formKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Registration Form'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Form submitted');
}
},
child: const Text('Submit'),
),
],
),
),
),
);
}
}
5. Using GlobalKey
A GlobalKey allows the application to access the state of a Form from the widget that owns the key.
final _formKey = GlobalKey();
The key is then assigned to the Form:
Form(
key: _formKey,
child: Column(
children: [
// Form fields
],
),
)
The form can then be validated:
if (_formKey.currentState!.validate()) {
print('Form is valid');
}
Flutter recommends using a GlobalKey to access a form when appropriate. The official documentation also provides Form.of() for accessing a FormState from a descendant context.
6. Structuring Forms with Sections
For larger forms, fields should be divided into meaningful sections.
Form
├── Personal Information
├── Contact Information
├── Account Information
├── Preferences
└── Submit Actions
In Flutter, sections can be visually separated using widgets such as Card, Container, Column, Padding, and Divider.
Example
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Personal Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
const Text(
'Contact Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
],
),
)
7. Personal Information Section
A personal information section generally collects basic details about the user.
Common Fields
- First Name
- Last Name
- Full Name
- Date of Birth
- Gender
Example
TextFormField(
decoration: const InputDecoration(
labelText: 'First Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: 'Last Name',
border: OutlineInputBorder(),
),
),
8. Contact Information Section
The contact section can contain email, phone number, address, city, and postal code fields.
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email Address',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
),
9. Account Information Section
Account-related information can include username, password, and password confirmation.
TextFormField(
decoration: const InputDecoration(
labelText: 'Username',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
),
),
10. TextFormField in Structured Forms
TextFormField is especially useful for structured forms because it combines text input with FormField functionality. This makes it possible to provide validators, save callbacks, initial values, and form-level validation.
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
return null;
},
)
The Flutter API documents TextFormField as a convenience widget that wraps a TextField in a FormField.
11. Adding Validation to Structured Forms
Every important field should have appropriate validation rules.
Required Field Validation
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
return null;
}
Email Validation
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email address';
}
return null;
}
Password Validation
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must contain at least 8 characters';
}
return null;
}
Flutter's validation workflow calls the validators associated with descendant form fields when FormState.validate() is called. If validation succeeds, the method returns true; otherwise, it returns false and displays the relevant errors.
12. Password Confirmation Validation
When creating registration forms, the confirmation password should match the original password.
final passwordController = TextEditingController();
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm your password';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
13. Using TextEditingController
TextEditingController allows developers to read, modify, and control the text entered into a text field.
final nameController = TextEditingController();
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
);
To retrieve the entered value:
String name = nameController.text;
Controllers should be disposed when they are no longer needed.
@override
void dispose() {
nameController.dispose();
super.dispose();
}
14. Dropdown Fields
Structured forms frequently require users to select one option from a predefined list.
String? selectedRole;
DropdownButtonFormField(
value: selectedRole,
decoration: const InputDecoration(
labelText: 'Select Role',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'student',
child: Text('Student'),
),
DropdownMenuItem(
value: 'developer',
child: Text('Developer'),
),
DropdownMenuItem(
value: 'designer',
child: Text('Designer'),
),
],
onChanged: (value) {
setState(() {
selectedRole = value;
});
},
validator: (value) {
if (value == null) {
return 'Please select a role';
}
return null;
},
)
15. Checkbox in a Structured Form
Checkboxes are useful when the user needs to accept a condition or enable an option.
bool acceptedTerms = false;
CheckboxListTile(
title: const Text('I accept the Terms and Conditions'),
value: acceptedTerms,
onChanged: (value) {
setState(() {
acceptedTerms = value ?? false;
});
},
)
Validating Terms and Conditions
if (!acceptedTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please accept the terms and conditions'),
),
);
return;
}
16. Radio Buttons in Structured Forms
Radio buttons allow the user to select one option from a group.
String? gender;
Column(
children: [
RadioListTile(
title: const Text('Male'),
value: 'male',
groupValue: gender,
onChanged: (value) {
setState(() {
gender = value;
});
},
),
RadioListTile(
title: const Text('Female'),
value: 'female',
groupValue: gender,
onChanged: (value) {
setState(() {
gender = value;
});
},
),
],
)
17. Date Picker in a Form
A date picker can be used when the form requires a date such as date of birth or appointment date.
DateTime? selectedDate;
Future selectDate() async {
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1950),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
setState(() {
selectedDate = pickedDate;
});
}
}
A button can trigger the date picker:
ElevatedButton(
onPressed: selectDate,
child: const Text('Select Date'),
)
18. Multi-Line Text Fields
For addresses, descriptions, comments, and messages, a multiline field can be used.
TextFormField(
maxLines: 4,
decoration: const InputDecoration(
labelText: 'Address',
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Address is required';
}
return null;
},
)
19. Form Layout with SingleChildScrollView
Large forms can exceed the available screen height, especially when the keyboard appears. SingleChildScrollView can make the form vertically scrollable.
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
// Form fields
],
),
),
)
This approach is particularly useful for mobile registration and profile forms containing many fields.
20. Using Cards for Form Sections
Cards can visually separate different sections of a long form.
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Personal Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
),
],
),
),
)
21. Creating Reusable Form Sections
Large applications benefit from reusable widgets. Instead of placing every field inside one large build method, separate related sections into widgets.
Example
class PersonalInformationSection extends StatelessWidget {
final TextEditingController nameController;
const PersonalInformationSection({
super.key,
required this.nameController,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Personal Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
),
],
);
}
}
This makes the main form easier to read and maintain.
22. Saving Form Data
The onSaved callback can be used to copy validated field values into variables or a data model when FormState.save() is called.
String name = '';
TextFormField(
decoration: const InputDecoration(
labelText: 'Name',
),
onSaved: (value) {
name = value ?? '';
},
)
Then call:
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print(name);
}
23. Resetting a Structured Form
Use FormState.reset() to reset form fields to their initial values.
ElevatedButton(
onPressed: () {
_formKey.currentState!.reset();
},
child: const Text('Reset'),
)
A reset button is useful for forms where users may want to clear their current entries and start again.
24. Form Submission Flow
A good structured form normally follows this sequence:
- User enters information.
- Application validates the fields.
- If validation fails, errors are displayed.
- If validation succeeds, the form data is saved or collected.
- Application performs the required action.
- A success or error message is displayed.
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form submitted successfully'),
),
);
}
}
25. Complete Structured Registration Form
The following example combines multiple concepts into one practical registration form.
import 'package:flutter/material.dart';
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State {
final _formKey = GlobalKey();
final nameController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final phoneController = TextEditingController();
String? selectedRole;
bool acceptedTerms = false;
@override
void dispose() {
nameController.dispose();
emailController.dispose();
passwordController.dispose();
phoneController.dispose();
super.dispose();
}
void submitForm() {
if (!_formKey.currentState!.validate()) {
return;
}
if (!acceptedTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please accept the terms and conditions'),
),
);
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Registration successful'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create Account'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Personal Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your name';
}
return null;
},
),
const SizedBox(height: 16),
const Text(
'Contact Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Enter a valid email address';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your phone number';
}
if (value.length < 10) {
return 'Enter a valid phone number';
}
return null;
},
),
const SizedBox(height: 16),
const Text(
'Account Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a password';
}
if (value.length < 8) {
return 'Password must contain at least 8 characters';
}
return null;
},
),
const SizedBox(height: 16),
DropdownButtonFormField(
value: selectedRole,
decoration: const InputDecoration(
labelText: 'Role',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'student',
child: Text('Student'),
),
DropdownMenuItem(
value: 'developer',
child: Text('Developer'),
),
DropdownMenuItem(
value: 'designer',
child: Text('Designer'),
),
],
onChanged: (value) {
setState(() {
selectedRole = value;
});
},
validator: (value) {
if (value == null) {
return 'Please select a role';
}
return null;
},
),
const SizedBox(height: 16),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'I accept the Terms and Conditions',
),
value: acceptedTerms,
onChanged: (value) {
setState(() {
acceptedTerms = value ?? false;
});
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
_formKey.currentState!.reset();
setState(() {
selectedRole = null;
acceptedTerms = false;
});
},
child: const Text('Reset'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: submitForm,
child: const Text('Register'),
),
),
],
),
],
),
),
),
);
}
}
26. Auto Validation
Flutter supports AutovalidateMode for controlling when validation should occur.
Validate Only When Submitted
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
child: ...
)
Validate After User Interaction
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ...
)
Always Validate
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.always,
child: ...
)
Using AutovalidateMode.onUserInteraction is often useful when the application should show validation feedback after the user starts interacting with a field.
27. Styling Structured Forms
A structured form should have a consistent visual design.
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email address',
prefixIcon: const Icon(Icons.email),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
)
Useful Styling Properties
labelText
hintText
helperText
prefixIcon
suffixIcon
filled
fillColor
border
enabledBorder
focusedBorder
errorBorder
28. Managing Focus Between Form Fields
Focus management makes long forms easier to use. A FocusNode can be used to move the user's focus from one field to another.
final emailFocusNode = FocusNode();
TextFormField(
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(emailFocusNode);
},
),
TextFormField(
focusNode: emailFocusNode,
keyboardType: TextInputType.emailAddress,
)
Remember to dispose manually created FocusNode objects.
@override
void dispose() {
emailFocusNode.dispose();
super.dispose();
}
29. Loading State During Submission
When form submission involves a network request, the UI can show a loading state to prevent repeated submissions.
bool isLoading = false;
Future submitForm() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
isLoading = true;
});
await Future.delayed(const Duration(seconds: 2));
setState(() {
isLoading = false;
});
}
Button example:
ElevatedButton(
onPressed: isLoading ? null : submitForm,
child: isLoading
? const CircularProgressIndicator()
: const Text('Submit'),
)
30. Organizing Form Data with a Model
For larger applications, form data can be represented by a Dart model instead of keeping many independent variables.
class UserData {
final String name;
final String email;
final String phone;
final String role;
UserData({
required this.name,
required this.email,
required this.phone,
required this.role,
});
}
After validation, create an object:
final user = UserData(
name: nameController.text.trim(),
email: emailController.text.trim(),
phone: phoneController.text.trim(),
role: selectedRole!,
);
This approach becomes useful when form data needs to be sent to an API, database, repository, or state-management layer.
31. Form Architecture
A scalable form can be organized into separate layers.
RegistrationScreen
│
├── Form
│ ├── PersonalInformationSection
│ ├── ContactInformationSection
│ ├── AccountInformationSection
│ ├── PreferencesSection
│ └── TermsSection
│
├── Validation
│
├── Form Data Model
│
└── Submission Logic
This structure keeps UI, validation, data handling, and submission logic easier to maintain.
32. Common Form Operations
| Operation |
Code |
Purpose |
| Validate |
_formKey.currentState!.validate() |
Checks all form validators. |
| Save |
_formKey.currentState!.save() |
Runs onSaved callbacks. |
| Reset |
_formKey.currentState!.reset() |
Restores fields to their initial state. |
| Read Text |
controller.text |
Retrieves entered text. |
| Clear Text |
controller.clear() |
Removes controller text. |
33. Common Mistakes When Creating Structured Forms
1. Creating a New GlobalKey Inside build()
Do not recreate the form key every time the widget builds.
// Avoid
Widget build(BuildContext context) {
final formKey = GlobalKey();
...
}
Instead, create it as a state field:
final _formKey = GlobalKey();
2. Forgetting to Validate
onPressed: () {
// Do not directly submit without validation.
}
Use:
onPressed: () {
if (_formKey.currentState!.validate()) {
// Submit data
}
}
3. Forgetting to Dispose Controllers
@override
void dispose() {
nameController.dispose();
emailController.dispose();
super.dispose();
}
4. Putting Too Many Fields in One Unstructured Column
Large forms should be divided into logical sections so users can understand which information belongs together.
5. Not Handling Keyboard Overflow
Use a scrollable layout such as SingleChildScrollView for long mobile forms.
34. Best Practices for Structured Forms
- Group related fields together.
- Use clear section headings.
- Use meaningful labels.
- Provide appropriate keyboard types.
- Validate required fields.
- Display understandable error messages.
- Use
TextFormField when integrating text fields with Form validation.
- Use controllers when programmatic access to text is required.
- Dispose controllers and manually created focus nodes.
- Use scrolling for long forms.
- Disable repeated submission while processing.
- Keep validation logic consistent.
- Use reusable widgets for repeated sections.
- Separate form UI from business and submission logic.
- Keep the number of fields manageable where possible.
- Make forms usable on different screen sizes.
35. Structured Form Example: Login Form
final _formKey = GlobalKey();
final emailController = TextEditingController();
final passwordController = TextEditingController();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Minimum 8 characters required';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Login submitted');
}
},
child: const Text('Login'),
),
],
),
)
36. Structured Form Example: Contact Form
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
maxLines: 5,
decoration: const InputDecoration(
labelText: 'Message',
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Message is required';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Message submitted');
}
},
child: const Text('Send Message'),
),
],
),
)
37. Structured Form Development Workflow
- Identify the information that needs to be collected.
- Group related fields into sections.
- Create a StatefulWidget when local form state is required.
- Create a
GlobalKey.
- Place the fields inside a
Form.
- Use appropriate form field widgets.
- Add controllers when values need to be accessed programmatically.
- Add validation rules.
- Add focus management where useful.
- Add submit and reset buttons.
- Validate the form before processing data.
- Convert validated values into a suitable data model if necessary.
- Send or save the data.
- Show success or error feedback.
- Test the form on different screen sizes.
38. Form Validation Checklist
- Is every required field validated?
- Are email addresses checked?
- Are passwords checked?
- Are confirmation fields compared with their original values?
- Are dropdown selections validated?
- Are required checkboxes handled?
- Are error messages clear?
- Does the form prevent invalid submission?
- Does the reset button work correctly?
- Are controllers disposed?
- Does the form scroll when the keyboard appears?
- Is the submit button protected against repeated submissions?
39. Structured Forms vs Unstructured Input
| Structured Form |
Unstructured Input |
| Fields are grouped logically. |
Fields may appear without clear organization. |
| Validation can be centralized. |
Validation may be scattered. |
| Easy to maintain. |
Can become difficult to maintain. |
| Supports FormState operations. |
Usually requires manual handling. |
| Better for registration, login, checkout, profile, and contact forms. |
Suitable for simple individual inputs. |
40. Interview Questions
Q1. What is a Form in Flutter?
A Form is a widget used to group multiple form fields and manage operations such as validation, saving, and resetting.
Q2. Why is GlobalKey used?
It provides access to the FormState so developers can call methods such as validate(), save(), and reset().
Q3. What is TextFormField?
TextFormField is a form-integrated text input widget that wraps a TextField with FormField functionality.
Q4. How do you validate a form?
if (_formKey.currentState!.validate()) {
// Form is valid
}
Q5. How do you reset a form?
_formKey.currentState!.reset();
Q6. How do you save form data?
_formKey.currentState!.save();
Q7. Why should large forms be divided into sections?
Sections improve readability, navigation, usability, maintainability, and the organization of validation and data handling.
Q8. Why should TextEditingController be disposed?
Controllers hold resources and should be disposed when they are no longer needed.
41. Quick Revision
Form groups multiple form fields.
FormField represents an individual form field.
TextFormField provides text input with Form integration.
GlobalKey provides access to form state.
validate() checks field validators.
save() calls field onSaved callbacks.
reset() restores fields to their initial values.
TextEditingController manages text input.
DropdownButtonFormField can be used for structured selection fields.
SingleChildScrollView helps handle long forms.
- Large forms should be divided into logical sections.
- Controllers and manually created FocusNodes should be disposed.
- Validation should happen before form submission.
42. Official Flutter Resources
43. JustAcademy Flutter Course Resources
Learn more about Flutter development through the following resources:
44. Key Takeaways
- Structured forms provide an organized way to collect user information.
- The
Form widget can contain multiple form fields.
- Use
TextFormField for text fields that participate in form validation.
- Use
GlobalKey when you need to access the form state.
- Divide large forms into logical sections such as personal, contact, account, and preference information.
- Use validators to prevent invalid data from being submitted.
- Use controllers when form values need to be read or modified programmatically.
- Use
SingleChildScrollView for long forms.
- Use reusable widgets to keep large forms clean and maintainable.
- Validate, save, and process the form data before sending it to an API or database.